/blog/
/blog/one/
/blog/resume/
/blog/answer/
/blog/code/
/blog/know/
/blog/posts/1109. 航班预订统计/
/blog/posts/1143. 最长公共子序列/
/blog/posts/1221. 分割平衡字符串/
/blog/posts/1436. 旅行终点站/
/blog/posts/1480. 一维数组的动态和/
/blog/posts/1588. 所有奇数长度子数组的和/
/blog/posts/162. 寻找峰值/
/blog/posts/1646. 获取生成数组中的最大值/
/blog/posts/165. 比较版本号/
/blog/posts/166. 分数到小数/
/blog/posts/187. 重复的DNA序列/
/blog/posts/1894. 找到需要补充粉笔的学生编号/
/blog/posts/208. 实现 Trie (前缀树)/
/blog/posts/211. 添加与搜索单词 - 数据结构设计/
/blog/posts/212. 单词搜索 II/
/blog/posts/223. 矩形面积/
/blog/posts/229. 求众数 II/
/blog/posts/230. 二叉搜索树中第K小的元素/
/blog/posts/240. 搜索二维矩阵 II/
/blog/posts/260. 只出现一次的数字 III/
/blog/posts/273. 整数转换英文表示/
/blog/posts/282. 给表达式添加运算符/
/blog/posts/284. 顶端迭代器/
/blog/posts/292. Nim 游戏/
/blog/posts/299. 猜数字游戏/
/blog/posts/301. 删除无效的括号/
/blog/posts/326. 3的幂/
/blog/posts/335. 路径交叉/
/blog/posts/352. 将数据流变为多个不相交区间/
/blog/posts/36. 有效的数独/
/blog/posts/371. 两整数之和/
/blog/posts/38. 外观数列/
/blog/posts/407. 接雨水 II/
/blog/posts/405. 数字转换为十六进制数/
/blog/posts/430. 扁平化多级双向链表/
/blog/posts/437. 路径总和 III/
/blog/posts/447. 回旋镖的数量/
/blog/posts/453. 最小操作次数使数组元素相等/
/blog/posts/470. 用 Rand7() 实现 Rand10()/
/blog/posts/476. 数字的补数/
/blog/posts/502. IPO/
/blog/posts/517. 超级洗衣机/
/blog/posts/524. 通过删除字母匹配到字典里最长单词/
/blog/posts/528. 按权重随机选择/
/blog/posts/600. 不含连续1的非负整数/
/blog/posts/639. 解码方法 II/
/blog/posts/66. 加一/
/blog/posts/678. 有效的括号字符串/
/blog/posts/68. 文本左右对齐/
/blog/posts/704. 二分查找/
/blog/posts/725. 分隔链表/
/blog/posts/789. 逃脱阻碍者/
/blog/posts/787. K 站中转内最便宜的航班/
/blog/posts/869. 重新排序得到 2 的幂/
/blog/posts/881. 救生艇/
/blog/posts/797. 所有可能的路径/
/blog/posts/
/blog/posts/剑指 Offer 10- I. 斐波那契数列/
/blog/posts/剑指 Offer 22. 链表中倒数第k个节点/
/blog/posts/面试题 17.14. 最小K个数/
/blog/workshop/
/blog/answer/common/
/blog/answer/interview/
/blog/answer/invest/
/blog/answer/webRTC/01-前置知识/
/blog/answer/webRTC/02-RTCPeerConnection/
/blog/answer/webRTC/03-实践/
/blog/answer/webRTC/
/blog/code/lodash/01/
/blog/code/lodash/
/blog/code/nuxt/
/blog/code/npm/01-pify/
/blog/code/npm/02-downlaod/
/blog/code/npm/03-video.js/
/blog/code/npm/04-craco/
/blog/code/npm/05-axios/
/blog/code/npm/
/blog/code/opensource/01-第一期/
/blog/code/opensource/02-第二期/
/blog/code/opensource/04-第四期/
/blog/code/opensource/05-第五期/
/blog/code/opensource/07-第七期/
/blog/code/opensource/08-第八期/
/blog/code/opensource/09-第九期/
/blog/code/opensource/10-第十期/
/blog/code/opensource/11-第十一期 玩具vite/
/blog/code/opensource/12-第十二期 ni/
/blog/code/opensource/13-第十三期 open/
/blog/code/opensource/14-第十四期 promisify/
/blog/code/opensource/15-第十五期 element新增组件功能/
/blog/code/opensource/16-第十六期 一行代码统一规范 包管理器/
/blog/code/opensource/17-第十七期 js-cookie/
/blog/code/opensource/18-第十八期 delay/
/blog/code/opensource/21-第未知期/
/blog/code/opensource/
/blog/code/react/
/blog/code/vitepress/
/blog/know/back/
/blog/know/computer/
/blog/know/front/
/blog/know/javascript/
/blog/know/network/01-网路/
/blog/know/network/
/blog/know/tool/
/blog/know/typescript/
/blog/workshop/cli/01-项目创建/
/blog/workshop/cli/02-npm包发布/
/blog/workshop/cli/
/blog/workshop/windows11/
/blog/answer/common/business/vuepress搭建之旅/
/blog/answer/common/business/编写代码之前的思考/
/blog/answer/common/offer/03. 数组中重复的数字/
/blog/answer/common/interview/
/blog/answer/common/offer/04. 二维数组中的查找/
/blog/answer/common/offer/05. 替换空格/
/blog/answer/common/offer/07. 重建二叉树/
/blog/answer/common/offer/06. 从尾到头打印链表/
/blog/answer/common/offer/09. 用两个栈实现队列/
/blog/answer/common/offer/10- I. 斐波那契数列/
/blog/answer/common/offer/10- II. 青蛙跳台阶问题/
/blog/answer/common/offer/11. 旋转数组的最小数字/
/blog/answer/common/offer/
/blog/answer/common/offer/剑指 Offer 10- I. 斐波那契数列/
/blog/answer/common/offer/剑指 Offer II 069. 山峰数组的顶部/
/blog/answer/common/web/
/blog/answer/common/offer/剑指 Offer 22. 链表中倒数第k个节点/
/blog/answer/common/web/拖拽/
/blog/answer/common/web/类型判断/
/blog/answer/invest/book/
/blog/answer/invest/book/不可不知的经济真相/
/blog/answer/invest/book/投资第一课/
/blog/answer/invest/book/纳瓦尔宝典/
/blog/answer/interview/basic/01-html/
/blog/answer/interview/basic/02-css/
/blog/answer/interview/basic/03-javascript/
/blog/answer/interview/basic/
/blog/answer/interview/basic/vue/
/blog/code/react/react/
/blog/answer/webRTC/janus/01-init/
/blog/answer/webRTC/janus/03-attach/
/blog/answer/webRTC/janus/
/blog/code/react/redux/
/blog/code/react/router/01-环境设置/
/blog/answer/webRTC/janus/02-janus/
/blog/code/react/router/
/blog/know/back/Egg/01-基础/
/blog/know/back/Egg/egg兼容mysql和mogodb/
/blog/know/back/Egg/
/blog/know/back/nodejs/01-install/
/blog/know/back/nodejs/98-process/
/blog/know/back/nodejs/99-file/
/blog/know/back/nodejs/
/blog/know/computer/algorithm/01-多选投票算法/
/blog/know/computer/algorithm/02-二叉树的各种遍历/
/blog/know/computer/algorithm/03-位运算/
/blog/know/computer/algorithm/04-距离相关/
/blog/know/computer/algorithm/06-线性表/
/blog/know/computer/algorithm/05-字符串/
/blog/know/computer/algorithm/07-队列/
/blog/know/computer/algorithm/08-栈/
/blog/know/computer/algorithm/09-哈希表/
/blog/know/computer/algorithm/10-dfs/
/blog/know/computer/algorithm/11-bfs/
/blog/know/computer/algorithm/
/blog/know/computer/data/01-队列/
/blog/know/computer/data/03-链表/
/blog/know/computer/data/04-树/
/blog/know/computer/data/05-栈/
/blog/know/computer/data/06-其他/
/blog/know/computer/data/06-堆/
/blog/know/computer/data/
/blog/know/computer/dayOne/1011. 在 D 天内送达包裹的能力/
/blog/know/computer/dayOne/1310. 子数组异或查询/
/blog/know/computer/dayOne/137. 只出现一次的数字 II/
/blog/know/computer/dayOne/1473. 粉刷房子 III/
/blog/know/computer/dayOne/1482. 制作 m 束花所需的最少天数/
/blog/know/computer/dayOne/1486. 数组异或操作/
/blog/know/computer/dayOne/1720. 解码异或后的数组/
/blog/know/computer/dayOne/1723. 完成所有工作的最短时间/
/blog/know/computer/dayOne/1734. 解码异或后的排列/
/blog/know/computer/dayOne/554. 砖墙/
/blog/know/computer/dayOne/633. 平方数之和/
/blog/know/computer/dayOne/690. 员工的重要性/
/blog/know/computer/dayOne/7. 整数反转/
/blog/know/computer/dayOne/403. 青蛙过河/
/blog/know/computer/dayOne/740. 删除并获得点数/
/blog/know/computer/dayOne/872. 叶子相似的树/
/blog/know/computer/dayOne/938. 二叉搜索树的范围和/
/blog/know/computer/dayOne/
/blog/know/computer/network/01-网路协议/
/blog/know/computer/network/98-关于options请求/
/blog/know/computer/network/
/blog/know/engineering/babel/
/blog/know/engineering/npm/78-npm push/
/blog/know/engineering/npm/
/blog/know/engineering/react/01-简介/
/blog/know/engineering/react/03-react-router/
/blog/know/engineering/react/04-hooks/
/blog/know/engineering/react/
/blog/know/engineering/webpack/01-基础/
/blog/know/engineering/webpack/02-loader/
/blog/know/engineering/webpack/03-plugin/
/blog/know/engineering/webpack/
/blog/know/front/css/01-选择器/
/blog/know/front/css/02-盒模型/
/blog/know/front/css/03-布局/
/blog/know/front/css/04-文本属性/
/blog/know/front/css/
/blog/know/front/network/01-网路/
/blog/know/front/network/
/blog/know/front/html/01-head/
/blog/know/front/html/02-body/
/blog/know/front/html/09-canvas/
/blog/know/front/html/10-svg/
/blog/know/front/html/
/blog/know/front/react/01-xx/
/blog/know/front/react/02-props/
/blog/know/front/react/03-state/
/blog/know/front/react/04-Lifecycle/
/blog/know/front/react/05-hook/
/blog/know/front/react/06-redux/
/blog/know/front/react/
/blog/know/front/webRTC/
/blog/know/front/webpack/01-基础/
/blog/know/front/webpack/02-loader/
/blog/know/front/webpack/03-plugin/
/blog/know/front/webpack/
/blog/know/javascript/BOM和DOM/01-navigator/
/blog/know/javascript/BOM和DOM/
/blog/know/javascript/advance/07-迭代器与生成器/
/blog/know/javascript/advance/11-promise/
/blog/know/javascript/advance/12-正则表达式/
/blog/know/javascript/advance/
/blog/know/javascript/basic/02-类/
/blog/know/javascript/basic/03-数据类型/
/blog/know/javascript/basic/04-函数进阶/
/blog/know/javascript/basic/
/blog/know/javascript/api/99-promise/
/blog/know/javascript/basic/01-语言基础/
/blog/know/javascript/basic/05-原型链/
/blog/know/tool/chromedevtools/
/blog/know/tool/git/01-基础/
/blog/know/tool/git/02-log/
/blog/know/tool/git/03-checkout/
/blog/know/tool/git/09-submodule/
/blog/know/tool/git/04-diff/
/blog/know/tool/git/10-workflow/
/blog/know/tool/git/
/blog/know/tool/vscode/
/blog/know/typescript/basic/01-基础概念/
/blog/know/typescript/basic/02-基础类型/
/blog/know/typescript/basic/03-接口/
/blog/know/typescript/basic/04-类/
/blog/know/typescript/basic/05-函数/
/blog/know/typescript/basic/06-泛型/
/blog/know/typescript/basic/07-枚举/
/blog/know/typescript/basic/08-高级类型/
/blog/know/typescript/basic/09-模块/
/blog/know/typescript/basic/10-模块解析/
/blog/know/typescript/basic/11-命名空间/
/blog/know/typescript/basic/
/blog/know/typescript/declarationfiles/01-示例/
/blog/know/typescript/declarationfiles/02-结构/
/blog/know/typescript/declarationfiles/03-模板/
/blog/know/typescript/declarationfiles/04-最佳实践/
/blog/know/typescript/declarationfiles/05-深入/
/blog/know/typescript/declarationfiles/

尤雨溪推荐 的 ni 神器

环境准备

git clone https://github.com/antfu/ni.git

package.json

"bin": {
  "ni": "bin/ni.js",
  "nci": "bin/nci.js",
  "nr": "bin/nr.js",
  "nu": "bin/nu.js",
  "nx": "bin/nx.js",
  "nrm": "bin/nrm.js"
},
"scripts": {
  "prepublishOnly": "rimraf dist && npm run build",
  "watch": "npm run build -- --watch",
  "?ni": "npm install",
  "ni": "esno src/ni.ts",
  "nci": "esno src/nci.ts",
  "nr": "esno src/nr.ts",
  "nu": "esno src/nu.ts",
  "nx": "esno src/nx.ts",
  "nrm": "esno src/nrm.ts",
  "dev": "esno src/ni.ts",
  "build": "rimraf dist && tsup src/ni.ts src/nci.ts src/nr.ts src/nu.ts src/nx.ts src/nrm.ts src/index.ts --format cjs,esm --dts",
  "release": "npx git-ensure && npx bumpp --commit --push --tag",
  "lint": "eslint \"**/*.ts\"",
  "lint:fix": "npm run lint -- --fix",
  "test": "c8 ava"
},
  "devDependencies": {
    "@antfu/eslint-config-ts": "^0.7.0",
    "@types/ini": "^1.3.30",
    "@types/node": "^16.7.10",
    "@types/prompts": "^2.4.0",
    "ava": "^3.15.0",
    "c8": "^7.8.0",
    "esbuild-register": "^3.0.0",
    "eslint": "^7.32.0",
    "esm": "^3.2.25",
    "esno": "^0.9.1",
    "execa": "^5.1.1",
    "find-up": "^5.0.0",
    "ini": "^2.0.0",
    "prompts": "^2.4.1",
    "rimraf": "^3.0.2",
    "terminal-link": "^3.0.0",
    "tsup": "^4.14.0",
    "typescript": "^4.4.2"
  },

一些三方依赖库

  • execa: child_process 的增强版,对命令执行更加人性化
  • find-up: 通过遍历父目录查找文件或目录
  • prompts: ini 配置文件的写入和读取

bin命令

  • "ni": "bin/ni.js" - install
  • "nci": "bin/nci.js" - clean install
  • "nr": "bin/nr.js" - run
  • "nu": "bin/nu.js" - upgrade
  • "nx": "bin/nx.js" - execute
  • "nrm": "bin/nrm.js - remove

这里可以看出,当在命令行中敲出命令的时候,背后运行的js文件,注意这个项目使用 ts 进行编写的, 所有存在一个 build 命令

  • "build": "rimraf dist && tsup src/ni.ts src/nci.ts src/nr.ts src/nu.ts src/nx.ts src/nrm.ts src/index.ts --format cjs,esm --dts"

  • rimraf: node版本 的 rm -rf

  • tsup: 无需配置便可以 使用 ts,基础配置由 esbuild 提供, --dts 可以生成 d.ts 声明文件

ni & nu & nx & nrm

// ni.ts
import { parseNi } from './commands'
// nu.ts
import { parseNu } from './commands'
// nx.ts
import { parseNx } from './commands'
// nrm.ts
import { parseNrm } from './commands'

import { runCli } from './runner'

runCli(parseNi)

nci.ts

import { parseNi } from './commands'
import { runCli } from './runner'

runCli(
  (agent, _, hasLock) => parseNi(agent, ['--frozen-if-present'], hasLock),
  { autoInstall: true },
)

总结

从现在已有代码来看,有两个核心的功能

  • commands 中 parseNx,parseNu,parseNrm,parseNi 等函数
  • runCli

runner.ts

runCLi所在的代码文件

import { resolve } from 'path'
import prompts from 'prompts'
import execa from 'execa'
import { Agent, agents } from './agents'
import { getDefaultAgent, getGlobalAgent } from './config'
import { detect, DetectOptions } from './detect'
import { remove } from './utils'

const DEBUG_SIGN = '?'

export interface RunnerContext {
  hasLock?: boolean
  cwd?: string
}

export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) => Promise<string | undefined> | string | undefined

export async function runCli(fn: Runner, options: DetectOptions = {}) {
  // process.argv 第一个元素 执行路径,第二个元素 执行js的文件路径

  // filter 中 真值 进行过滤 
  const args = process.argv.slice(2).filter(Boolean)
  try {
    // 尝试使用 run 调用函数
    await run(fn, args, options)
  }
  catch (error) {
    process.exit(1)
  }
}

// run 主体函数
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
  // debug 参数
  const debug = args.includes(DEBUG_SIGN)
  if (debug)
    remove(args, DEBUG_SIGN)
  // process.cwd() 返回 Node.js 进程的当前工作目录
  let cwd = process.cwd()
  let command

  if (args[0] === '-C') {
    // 将路径或路径片段的序列解析为绝对路径
    cwd = resolve(cwd, args[1])
    // 删除 0 1 参数
    args.splice(0, 2)
  }
  // 是否为全局
  const isGlobal = args.includes('-g')
  if (isGlobal) {
    // 默认配置 全局代理 为 npm
    command = await fn(getGlobalAgent(), args)
  }
  else {
    // 借助 detect 使用 lock 文件 拿到使用的 包管理器,并可能会全局安装所需包管理器
    let agent = await detect({ ...options, cwd }) || getDefaultAgent()
    if (agent === 'prompt') {
      // agent 手动选择 包管理器
      agent = (await prompts({
        name: 'agent',
        type: 'select',
        message: 'Choose the agent',
        choices: agents.map(value => ({ title: value, value })),
      })).agent
      if (!agent)
        return
    }
    // 生成命令
    command = await fn(agent as Agent, args, {
      hasLock: Boolean(agent),
      cwd,
    })
  }
  // 命令为空 直接放回
  if (!command)
    return

  // 命令为空 直接放回  DEBUG 状态 打印命令不执行
  if (debug) {
    // eslint-disable-next-line no-console
    console.log(command)
    return
  }

  // 借助 execa 库 来执行命令
  await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}

commands

这里提供了几个转义命令的功能函数

function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
) {
  if (!(agent in AGENTS))
    throw new Error(`Unsupported agent "${agent}"`)
  // 从常量文件中 得到命令字符串
  const c = AGENTS[agent][command]

  if (typeof c === 'function')
    return c(args)

  if (!c)
    throw new Error(`Command "${command}" is not support by agent "${agent}"`)
  // 替换 留出的文件
  return c.replace('{0}', args.join(' ')).trim()
}

parseNi

const parseNi = <Runner>((agent, args, ctx) => {
  if (args.length === 1 && args[0] === '-v') {
    // eslint-disable-next-line no-console
    console.log(`@antfu/ni v${version}`)
    process.exit(0)
  }

  if (args.length === 0)
    return getCommand(agent, 'install')

  if (args.includes('-g'))
    // exclude 从数组中删除某个字符串
    return getCommand(agent, 'global', exclude(args, '-g'))

  if (args.length === 1 && args[0] === '-f')
    return getCommand(agent, 'install', args)

  if (args.includes('--frozen-if-present')) {
    args = exclude(args, '--frozen-if-present')
    return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
  }

  if (args.includes('--frozen'))
    return getCommand(agent, 'frozen', exclude(args, '--frozen'))

  return getCommand(agent, 'add', args)
})

parseNr

const parseNr = <Runner>((agent, args) => {
  if (args.length === 0)
    args.push('start')

  if (args.includes('--if-present')) {
    args = exclude(args, '--if-present')
    args[0] = `--if-present ${args[0]}`
  }

  return getCommand(agent, 'run', args)
})

parseNu

const parseNu = <Runner>((agent, args) => {
  if (args.includes('-i'))
    return getCommand(agent, 'upgrade-interactive', exclude(args, '-i'))

  return getCommand(agent, 'upgrade', args)
})

parseNrm

const parseNrm = <Runner>((agent, args) => {
  if (args.includes('-g'))
    return getCommand(agent, 'global_uninstall', exclude(args, '-g'))
  return getCommand(agent, 'uninstall', args)
})

parseNx

const parseNx = <Runner>((agent, args) => {
  return getCommand(agent, 'execute', args)
})

工具类函数

exclude

function remove<T>(arr: T[], v: T) {
  const index = arr.indexOf(v)
  if (index >= 0)
    arr.splice(index, 1)

  return arr
}

// 将传入的数据 从数组中删除(为浅拷贝,对原数组不产生影响)
function exclude<T>(arr: T[], v: T) {
  return remove(arr.slice(), v)
}

detect

async function detect({ autoInstall, cwd }: DetectOptions) {
  // 通过 找寻 locks 文件 来判断使用了哪一个包管理器
  const result = await findUp(Object.keys(LOCKS), { cwd })
  const agent = (result ? LOCKS[path.basename(result)] : null)

  if (agent && !cmdExists(agent)) {
    if (!autoInstall) {
      console.warn(`Detected ${agent} but it doesn't seem to be installed.\n`)

      if (process.env.CI)
        process.exit(1)

      const link = terminalLink(agent, INSTALL_PAGE[agent])
      const { tryInstall } = await prompts({
        name: 'tryInstall',
        type: 'confirm',
        message: `Would you like to globally install ${link}?`,
      })
      if (!tryInstall)
        process.exit(1)
    }

    await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
  }

  return agent
}

总结

这个包主要做了以下件事:

  1. 找到项目的包管理器,如果没有找到,就让用户选择一个包管理器
  2. 将命令进行转移成对应的包管理器命令,并执行。